/*-
 * CharacterGroupProcessor.java
 *
 * $LastChangedBy: stein $
 * $LastChangedRevision: 96 $
 * $LastChangedDate: 2007-02-15 19:53:47 +0000 (Thu, 15 Feb 2007) $
 * $HeadURL: svn://localhost/SMS/src/com/googlecode/attention/sms/comp/CharacterGroupProcessor.java $
 *
 * Copyright (c) 2007, Stein Gunnar Bakkeby
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     * Neither the name of Stein Gunnar Bakkeby nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.googlecode.attention.sms.comp;

import java.io.IOException;

import com.googlecode.attention.io.CSVReader;
import com.googlecode.attention.sms.pdu.CBS;
import com.googlecode.attention.util.IntArrayBuilder;
import com.googlecode.attention.util.Settings;

import static com.googlecode.attention.util.UtilLogStatements.*;

/** * This class represents the character group processor. *  * @author Stein Gunnar Bakkeby */
public class CharacterGroupProcessor {
  /* Field values */
  private int numberOfGroups = 0;
  private int[] transitionSymbols;
  private int previousSymbol = -1; // -1 means cleared
  private int previousGroup = 0;
  private int baseGroup = 0;
  private int   compCurrentGroup = baseGroup;
  private int decompCurrentGroup = baseGroup;
  private int columns;
  private int rows;
  private CH previousCH = null;
  private int cg_id = 0;
  
  /*
   * Fold tables and Group Membership bit flags.
   * 
   * Char Value
   * Group 0 Fold Table
   * Group 1 Fold Table
   * Group 2 Fold Table
   * Group 2 Member
   * Group 1 Member
   * Group 0 Member
   */
  private int[][] charGroup;
  
  /**
   * Constructor, creates the Character Group Processor
   * based upon a given compression header.
   * 
   * @param ch The compression header
   * @throws IOException If initialisation file cannot be read
   */
  public CharacterGroupProcessor(final CH ch) throws IOException {
    changeInitialisation(ch);
  }
  
  /**
   * This will change the character group initialisation parameters.
   * 
   * @param ch The compression header
   * @throws IOException If initialisation file cannot be read
   */
  public void changeInitialisation(final CH ch) throws IOException {
    if (previousCH == null
        || previousCH.isUpdateNeeded(ch, CH.CHANGE_CHARACTER_GROUP)) {
      final String lang  = CBS.parse(ch.getCLC()).toString().toLowerCase();
      final int[] ext    = ch.getExtensionValues();
      /* If change initialisation ID */
      if (ext[CH.CHANGE_CHARACTER_GROUP] != -1)
          cg_id = ext[CH.CHANGE_CHARACTER_GROUP];
      String path = this.getClass().getCanonicalName();
      path = path.substring(0, path.lastIndexOf("."));
      final String resource = path + "."
                            + lang
                            + "_chargroup_"
                            + cg_id;
      
      if (cg_id == 0) {
        numberOfGroups = 0;
        baseGroup = 0;
        compCurrentGroup = baseGroup;
        transitionSymbols = new int[0];
        charGroup = new int[0][0];
      } else {    
        final Settings s = new Settings(resource); // TODO fix?
        
        numberOfGroups = s.getInteger("number_of_groups");
        baseGroup      = s.getInteger("base_group");
        compCurrentGroup   = baseGroup;
        
        String file = s.getString("group_table");
        file = file.replace("<lang>", lang);
        file = file.replace("<id>", "" + cg_id);
        
        transitionSymbols = new int[numberOfGroups - 1];
        final int[] allTranitionSymbols = new int[] {
            CompressionProcessor.CTRL_SYMB_CG_TRANS_0,
            CompressionProcessor.CTRL_SYMB_CG_TRANS_1,
            CompressionProcessor.CTRL_SYMB_CG_TRANS_2,
            CompressionProcessor.CTRL_SYMB_CG_TRANS_3,
            CompressionProcessor.CTRL_SYMB_CG_TRANS_4,
            CompressionProcessor.CTRL_SYMB_CG_TRANS_5,
            CompressionProcessor.CTRL_SYMB_CG_TRANS_6
        };
        
        for (int i = 0; i < transitionSymbols.length; i++) {
          transitionSymbols[i] = allTranitionSymbols[i];
        }
        
        charGroup = readInitFile(file);
      }
    }
    previousCH = new CH(ch);
  }
  
  /**
   * This will read a file containing character group init information.
   * 
   * @param file The file to read from
   * @return A table of integers
   */
  private int[][] readInitFile(final String file) {
    final CSVReader reader = new CSVReader(
        ClassLoader.getSystemResourceAsStream(file), true);
    reader.read();
    
    final String[][] csv = reader.getCSV();
    columns = reader.getNumberOfColumns();
    rows    = reader.getNumberOfRows();
    final int[][] intCSV = new int[columns][rows];
    try {
      for (int c = 0; c < columns; c++) {
        for (int r = 0; r < rows; r++) {
          intCSV[c][r] = Integer.parseInt(csv[c][r]);
        }
      }
    } catch (NumberFormatException e) {
      UTIL_DEBUG.log(e);
    }
    return intCSV;
  }
  
  /**
   * This will compress a given symbol using character groups.
   * 
   * @param inputSymbol A single symbol to be encoded
   * @param lastSymbol True if this is the last symbol to be encoded
   * @return A sequence of encoded symbols
   */
  public IntArrayBuilder compress(final int inputSymbol,
                                  final boolean lastSymbol) {
    /* Algorithm variables */
    int inputGroup = -1;
    final IntArrayBuilder output = new IntArrayBuilder();
    /* Step 1 */
    int numberOfOutputSymbols = 0;
    
    /* Step 2 */
    int step = 3;
    if (inputSymbol == CompressionProcessor.CTRL_SYMB_KEYWORD) {
      if (previousSymbol != -1) {
        /*
         * If a previous input symbol is being held, add this as a "literal"
         * to the output sequence by calculating the value of the element
         * indicated by the value of the previous symbol in the fold table
         * associated with the group of the previous symbol and increment
         * the number of output symbols and clear the previous symbol.
         */
        output.append(encode(previousSymbol, previousGroup));
        numberOfOutputSymbols++;
        previousSymbol = -1;
      }
      /* Goto Step 9 */
    } else { // Goto Step 3
      /* Step 3 */
      /* If the input symbol is a member of no group or a member of the
       * current group, set the group for the input symbol to be the
       * current group.
       */
      if (exists(inputSymbol, compCurrentGroup)) {
        inputGroup = compCurrentGroup;
      }
      /*
       * Otherwise, if a previous input symbol is being held and the input
       * symbol is a member of the group of the previous symbol, set the
       * group for the input symbol to be that of the previous symbol.
       */
      if (inputGroup == -1 && previousSymbol != -1) {
        if (exists(inputSymbol, previousGroup)) {
          inputGroup = previousGroup;
        }
      }
      /*
       * Otherwise, test the input symbol for membership of each group in
       * ascending order of groups starting with group 0 and set the group for
       * the input symbol to be that for which membership is first detected.
       */
      if (inputGroup == -1) {
        for (int g = 0; g < numberOfGroups; g++) {
          if (exists(inputSymbol, g)) {
            inputGroup = g;
            break;
          }
        }
      }
      /* If member of no group, set the input group to the current group. */
      if (inputGroup == -1) {
        inputGroup = compCurrentGroup;
      }
      
      /* Step 4 */
      if (previousSymbol != -1) {
        if (previousGroup == inputGroup) {
          /*
           * Add the Character Group Transition symbol that indicates a
           * transition from the current group to the previous symbol group to
           * the output sequence and increment the number of output symbols.
           */
          output.append(getTransitionSymbol(compCurrentGroup, previousGroup));
          numberOfOutputSymbols++;
          /* Set the current group to the previous symbol group. */
          compCurrentGroup = previousGroup;
          
          /*
           * Encode the previous symbol by calculating the value of the
           * element indicated by the value of the previous symbol in the
           * fold table associated with the base group and add this value to
           * the output sequence and increment the number of output symbols.
           */
          output.append(encode(previousSymbol, baseGroup));
          numberOfOutputSymbols++;
          
          /*
           * Encode the input symbol by calculating the value of the
           * element indicated by the value of the input symbol in the
           * fold table associated with the base group and add this value to
           * the output sequence and increment the number of output symbols.
           */
          output.append(encode(inputSymbol, baseGroup));
          numberOfOutputSymbols++;
          
          /* Clear the previous symbol. */
          previousSymbol = -1;
          /* Goto Step 9 */
          step = 9;
        } else {
          /*
           * Otherwise, encode the previous symbol as a "literal" by
           * calculating the value of the element indicated by the value
           * of the previous symbol in the fold table associated with the
           * group of the previous symbol group and add this value to the
           * output sequence and increment the number of output symbols
           * and clear the previous symbol.
           */
          output.append(encode(previousSymbol, previousGroup));
          numberOfOutputSymbols++;
          previousSymbol = -1;
        }
      }
      
      /* Step 5 */
      if (step != 9) {
        /*
         * If the input symbol group is the base group and the current
         * group is not the base group, add the Character Group Transition
         * symbol that indicates a transition from the current group to the
         * base group to the output sequence and increment the number of
         * output symbols. Set the current group to be the base group.
         */
        if (inputGroup == baseGroup && compCurrentGroup != baseGroup) {
          output.append(getTransitionSymbol(compCurrentGroup, baseGroup));
          numberOfOutputSymbols++;
          compCurrentGroup = baseGroup;
        }
        
        /* Step 6 */
        /*
         * If the input symbol group is the base group or the current group:
         *    - Encode the input symbol by calculating the value of the element
         *      indicated by the value of the input symbol in the fold table
         *      associated with the base group and add this value to the output
         *      sequence and increment the number of output symbols.
         *      
         *    - Goto Step 9.
         */
        if (inputGroup == baseGroup || inputGroup == compCurrentGroup) {
          output.append(encode(inputSymbol, baseGroup));
          numberOfOutputSymbols++;
          step = 9;
        }
        
        /* Step 7 */
        /*
         * If the input symbol is the last symbol to be encoded:
         *    - Encode the input symbol as a "literal" by calculating the value
         *      of the element indicated by the value of the input symbol in
         *      the fold table associated with the group of the input symbol
         *      and add this value to the output sequence and increment the
         *      number of output symbols.
         *    
         *    - Goto Step 9.
         */
        if (step != 9 && lastSymbol) {
          output.append(encode(inputSymbol, inputGroup));
          numberOfOutputSymbols++;
          step = 9;
        }
        
        /* Step 8 */
        if (step != 9) {
          previousSymbol = inputSymbol;
          previousGroup  = inputGroup;
        }
      }
    }
    
    /* Step 9 */
    return output;
  }
  
  private int encode(final int symbol, final int group) {
    final int fold = 1 + group;
    for (int e = 0; e < rows; e++) {
      if (charGroup[0][e] == symbol ){
        return charGroup[fold][e];
      }
    }
    return symbol;
  }
  
  private boolean exists(final int symbol, final int group) {
    final int g = charGroup.length - 1 - group;
    for (int e = 0; e < rows; e++) {
      if (charGroup[0][e] == symbol
       && charGroup[g][e] == 1) {
        return true;
      }
    }
    return false;
  }
  
  /**
   * Example:
   *           ____New_Group____
   * Current  |___0_|___1_|___2_|
   * Group  0 |     | 260 | 259 |
   *        1 | 260 |     | 259 |
   *        2 |_260_|_259_|_____|      
   * 
   * @param oldGroup The old group id
   * @param newGroup The new group id
   * @return The transition symbol, or -1 of oldGroup == newGroup or
   *         the group id(s) are invalid.
   */
  private int getTransitionSymbol(final int oldGroup, final int newGroup) {
    if (oldGroup == newGroup) return -1;
    int j = 0;
    for (int i = 0; i < numberOfGroups; i++) {
      if (newGroup == i) {
        return transitionSymbols[j];
      }
      if (i != oldGroup) j++;
    }
    return -1;
  }
  
  private void makeTransition(final int transitionSymbol) {
    int j = 0;
    final int length = transitionSymbols.length;
    for (int i = 0; i < length; i++) {
      if (i == decompCurrentGroup) j++;
      if (transitionSymbol == transitionSymbols[i]) {
        decompCurrentGroup = i + j;
        break;
      }
    }
  }
  
  /**
   * This will reset the decompression algorithm. 
   */
  public void resetDecompression() {
    decompCurrentGroup = 0;
  }
  
  /**
   * This will reset the compression algorithm.
   */
  public void resetCompression() {
    compCurrentGroup = 0;
  }
  
  /**
   * This will decompress the input using character groups.
   * 
   * @param inputSymbol The symbol to decode/decompress
   * @return The decoded symbol or an empty string if no
   *         symbol has been generated
   */
  public IntArrayBuilder decompress(final int inputSymbol) {
    final IntArrayBuilder output = new IntArrayBuilder();
    int step = 2;
    
    /* Step 1 */
    int length = transitionSymbols.length;
    for (int i = 0; i < length; i++) {
      if (inputSymbol == transitionSymbols[i]) {
        makeTransition(inputSymbol);
        step = 3;
        break;
      }
    }
    
    /* Step 2 */
    if (step == 2) {
      if (decompCurrentGroup != baseGroup
          || exists(inputSymbol, decompCurrentGroup)) {
        int g = charGroup.length - 1 - decompCurrentGroup;
        boolean no_match = true;
        for (int e = 0; e < rows; e++){
          if (charGroup[0][e] == inputSymbol) {
            output.append(charGroup[1 + decompCurrentGroup][e]);
            no_match = false;
            break;
          }
        }
        if (no_match) output.append(inputSymbol);
      } else {
        output.append(inputSymbol);
      }
    }
    
    /* Step 3 */
    return output;
  }
}